/*
 * ! ******************************************************************************
 *
 * Pentaho Data Integration
 *
 * Copyright (C) 2002-2017 by Hitachi Vantara : http://www.pentaho.com
 *
 *******************************************************************************
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 *
 ******************************************************************************/

package org.pentaho.di.core.plugins;

import java.io.File;
import java.lang.annotation.Annotation;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSelectInfo;
import org.apache.commons.vfs2.FileSelector;
import org.pentaho.di.core.Const;
import org.pentaho.di.core.exception.KettlePluginException;
import org.yzbdl.lanius.core.exception.LaniusEngineDbException;
import org.pentaho.di.core.logging.DefaultLogLevel;
import org.pentaho.di.core.logging.LogChannel;
import org.pentaho.di.core.logging.LogLevel;
import org.pentaho.di.core.util.Utils;
import org.pentaho.di.core.vfs.KettleVFS;
import org.pentaho.di.core.xml.XMLHandler;
import org.pentaho.di.i18n.BaseMessages;
import org.pentaho.di.i18n.LanguageChoice;
import org.w3c.dom.Node;
import org.yzbdl.lanius.core.plugins.ClassLoaderCfg;
import org.yzbdl.lanius.core.plugins.metastore.LdrCategoryEntity;
import org.yzbdl.lanius.core.plugins.metastore.LdrClassificationEntity;
import org.yzbdl.lanius.core.plugins.metastore.LdrPluginEntity;
import org.yzbdl.lanius.core.plugins.service.CategoryService;
import org.yzbdl.lanius.core.plugins.service.ClassificationService;
import org.yzbdl.lanius.core.plugins.service.PluginService;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;

public abstract class BasePluginType implements PluginTypeInterface {
    protected static Class<?> PKG = BasePluginType.class; // for i18n purposes, needed by Translator2!!

    /**
     * 类型ID
     */
    protected String id;

    /**
     * 类型名称
     */
    protected String name;

    /**
     * 该类型插件所在目录, 可以是多个目录
     */
    protected List<PluginFolderInterface> pluginFolders;

    protected PluginRegistry registry;

    protected LogChannel log;

    protected Map<Class<?>, String> objectTypes = new HashMap<Class<?>, String>();

    protected boolean searchLibDir;

    Class<? extends Annotation> pluginType;

    protected ClassificationService classificationService = ClassificationService.getInstance();
    protected CategoryService categoryService = CategoryService.getInstance();
    protected PluginService pluginService = PluginService.getInstance();

    public BasePluginType(Class<? extends Annotation> pluginType) {
        this.pluginFolders = new ArrayList<PluginFolderInterface>();
        this.log = new LogChannel("Plugin type");

        registry = PluginRegistry.getInstance();
        this.pluginType = pluginType;
    }

    /**
     * @param id   The plugin type ID
     * @param name the name of the plugin
     */
    public BasePluginType(Class<? extends Annotation> pluginType, String id, String name) {
        this(pluginType);
        this.id = id;
        this.name = name;
    }

    /**
     * this is a utility method for subclasses so they can easily register which
     * folders contain plugins
     *
     * @param xmlSubfolder the sub-folder where xml plugin definitions can be found
     */
    protected void populateFolders(String xmlSubfolder) {
        pluginFolders.addAll(PluginFolder.populateFolders(xmlSubfolder));
    }

    public Map<Class<?>, String> getAdditionalRuntimeObjectTypes() {
        return objectTypes;
    }

    @Override
    public void addObjectType(Class<?> clz, String xmlNodeName) {
        objectTypes.put(clz, xmlNodeName);
    }

    @Override
    public String toString() {
        return name + "(" + id + ")";
    }

    /**
     * Let's put in code here to search for the step plugins..
     */
    @Override
    public void searchPlugins() throws KettlePluginException {
        registerNatives();
        registerPluginJars();
        registerXmlPlugins();
        registerFromDb();
    }

    protected abstract void registerNatives() throws KettlePluginException;

    protected abstract void registerXmlPlugins() throws KettlePluginException;

    protected void registerFromDb() throws KettlePluginException {}

    public Class<? extends Annotation> getPluginType() {
        return pluginType;
    }

    public void setPluginType(Class<? extends Annotation> pluginType) {
        this.pluginType = pluginType;
    }

    /**
     * @return the id
     */
    @Override
    public String getId() {
        return id;
    }

    /**
     * @param id the id to set
     */
    public void setId(String id) {
        this.id = id;
    }

    /**
     * @return the name
     */
    @Override
    public String getName() {
        return name;
    }

    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * @return the pluginFolders
     */
    @Override
    public List<PluginFolderInterface> getPluginFolders() {
        return pluginFolders;
    }

    /**
     * @param pluginFolders the pluginFolders to set
     */
    public void setPluginFolders(List<PluginFolderInterface> pluginFolders) {
        this.pluginFolders = pluginFolders;
    }

    protected static String getCodedTranslation(String codedString) {
        if (StrUtil.isBlank(codedString)) {
            return null;
        }
        if (codedString.startsWith("i18n:")) {
            String[] parts = codedString.split(":");
            if (parts.length != 3) {
                return codedString;
            } else {
                return BaseMessages.getString(parts[1], parts[2]);
            }
        } else {
            return codedString;
        }
    }

    /**
     * @param input
     * @param localizedMap
     * @return
     */
    protected String getAlternativeTranslation(String input, Map<String, String> localizedMap) {
        if (Utils.isEmpty(input)) {
            return null;
        }
        if (input.startsWith("i18n")) {
            return getCodedTranslation(input);
        } else {
            String defLocale = LanguageChoice.getInstance().getDefaultLocale().toString().toLowerCase();
            String alt = localizedMap.get(defLocale);
            if (!Utils.isEmpty(alt)) {
                return alt;
            }
            String failoverLocale = LanguageChoice.getInstance().getFailoverLocale().toString().toLowerCase();
            alt = localizedMap.get(failoverLocale);
            if (!Utils.isEmpty(alt)) {
                return alt;
            }
            // Nothing found?
            // Return the original!
            //
            return input;
        }
    }

    protected static String getTranslation(String string, String packageName, String altPackageName,
        Class<?> resourceClass) {
        if (string == null) {
            return null;
        }
        if (string.startsWith("i18n:")) {
            String[] parts = string.split(":");
            if (parts.length != 3) {
                return string;
            } else {
                return BaseMessages.getString(parts[1], parts[2]);
            }
        } else {
            // Try the default package name
            //
            String translation;
            if (!Utils.isEmpty(packageName)) {
                LogLevel oldLogLevel = DefaultLogLevel.getLogLevel();

                // avoid i18n messages for missing locale
                //
                DefaultLogLevel.setLogLevel(LogLevel.BASIC);

                translation = BaseMessages.getString(packageName, string, resourceClass);
                if (translation.startsWith("!") && translation.endsWith("!")) {
                    translation = BaseMessages.getString(PKG, string, resourceClass);
                }

                // restore loglevel, when the last alternative fails, log it when loglevel is
                // detailed
                //
                DefaultLogLevel.setLogLevel(oldLogLevel);
                if (!Utils.isEmpty(altPackageName)) {
                    if (translation.startsWith("!") && translation.endsWith("!")) {
                        translation = BaseMessages.getString(altPackageName, string, resourceClass);
                    }
                }
            } else {
                // Translations are not supported, simply keep the original text.
                //
                translation = string;
            }

            return translation;
        }
    }

    /**
     * 查找xml类型插件的定义文件(plugin.xml)
     *
     * @param folder
     * @return
     */
    protected List<FileObject> findPluginXmlFiles(String folder) {
        return findPluginFiles(folder, ".*\\/plugin\\.xml$");
    }

    /**
     * 查找某个目录下面符合正则匹配的文件
     *
     * @param folder
     * @param regex
     * @return
     */
    private List<FileObject> findPluginFiles(String folder, final String regex) {
        List<FileObject> list = new ArrayList<FileObject>();
        try {
            FileObject folderObject = KettleVFS.getFileObject(folder);
            FileObject[] files = folderObject.findFiles(new FileSelector() {

                @Override
                public boolean traverseDescendents(FileSelectInfo fileSelectInfo) throws Exception {
                    return true;
                }

                @Override
                public boolean includeFile(FileSelectInfo fileSelectInfo) throws Exception {
                    return fileSelectInfo.getFile().toString().matches(regex);
                }
            });
            if (files != null) {
                for (FileObject file : files) {
                    list.add(file);
                }
            }
        } catch (Exception e) {
            // ignore this: unknown folder, insufficient permissions, etc
        }
        return list;
    }

    /**
     * 从xml资源中注册一个插件
     *
     * @param pluginNode
     * @param path
     * @param pluginType
     * @param nativePlugin
     * @param pluginFolder
     * @return
     * @throws KettlePluginException
     */
    protected PluginInterface registerPluginFromXmlResource(Node pluginNode, String path,
        Class<? extends PluginTypeInterface> pluginType, boolean nativePlugin, URL pluginFolder)
        throws KettlePluginException {
        try {

            String id = XMLHandler.getTagAttribute(pluginNode, "id");
            String version = XMLHandler.getTagAttribute(pluginNode, "version");
            if (StrUtil.isBlank(version)) {
                version = "none";
            }
            boolean enterprise = false;
            String enterpriseStr = XMLHandler.getTagAttribute(pluginNode, "enterprise");
            if (StrUtil.isNotBlank(enterpriseStr) && enterpriseStr.equals("true")) {
                enterprise = Boolean.getBoolean(enterpriseStr);
            }

            String description = getTagOrAttribute(pluginNode, "description");
            String iconfile = getTagOrAttribute(pluginNode, "iconfile");
            String tooltip = getTagOrAttribute(pluginNode, "tooltip");
            String category = getTagOrAttribute(pluginNode, "category");
            String classname = getTagOrAttribute(pluginNode, "classname");
            // String errorHelpfile = getTagOrAttribute(pluginNode, "errorhelpfile");
            // String documentationUrl = getTagOrAttribute(pluginNode, "documentation_url");
            // String casesUrl = getTagOrAttribute(pluginNode, "cases_url");
            // String forumUrl = getTagOrAttribute(pluginNode, "forum_url");

            Node libsnode = XMLHandler.getSubNode(pluginNode, "libraries");
            int nrlibs = XMLHandler.countNodes(libsnode, "library");

            List<String> jarFiles = new ArrayList<String>();
            if (path != null) {
                for (int j = 0; j < nrlibs; j++) {
                    Node libnode = XMLHandler.getSubNodeByNr(libsnode, "library", j);
                    String jarfile = XMLHandler.getTagAttribute(libnode, "name");
                    jarFiles.add(new File(path + Const.FILE_SEPARATOR + jarfile).getAbsolutePath());
                }
            }

            // Localized categories, descriptions and tool tips
            //
            Map<String, String> localizedCategories = readPluginLocale(pluginNode, "localized_category", "category");
            category = getAlternativeTranslation(category, localizedCategories);

            Map<String, String> localDescriptions =
                readPluginLocale(pluginNode, "localized_description", "description");
            description = getAlternativeTranslation(description, localDescriptions);

            Map<String, String> localizedTooltips = readPluginLocale(pluginNode, "localized_tooltip", "tooltip");
            tooltip = getAlternativeTranslation(tooltip, localizedTooltips);

            String iconFilename = (path == null) ? iconfile : path + Const.FILE_SEPARATOR + iconfile;
            // String errorHelpFileFull = errorHelpfile;
            // if (!Utils.isEmpty(errorHelpfile)) {
            // errorHelpFileFull = (path == null) ? errorHelpfile : path + Const.FILE_SEPARATOR + errorHelpfile;
            // }

            Map<Class<?>, String> classMap = new HashMap<Class<?>, String>();

            PluginMainClassType mainClassTypesAnnotation = pluginType.getAnnotation(PluginMainClassType.class);
            // 记录mainClassType与mainClass的映射关系
            classMap.put(mainClassTypesAnnotation.value(), classname);

            // process annotated extra types
            PluginExtraClassTypes classTypesAnnotation = pluginType.getAnnotation(PluginExtraClassTypes.class);
            if (classTypesAnnotation != null) {
                for (int i = 0; i < classTypesAnnotation.classTypes().length; i++) {
                    Class<?> classType = classTypesAnnotation.classTypes()[i];
                    String className = getTagOrAttribute(pluginNode, classTypesAnnotation.xmlNodeNames()[i]);
                    // 记录@PluginExtraClassTypes注解中classTypes与xmlNodeNames类的映射关系
                    // 通常也是该插件的mainClass, repository中用到此种方式进行
                    classMap.put(classType, className);
                }
            }

            // process extra types added at runtime
            Map<Class<?>, String> objectMap = getAdditionalRuntimeObjectTypes();
            for (Map.Entry<Class<?>, String> entry : objectMap.entrySet()) {
                String clzName = getTagOrAttribute(pluginNode, entry.getValue());
                classMap.put(entry.getKey(), clzName);
            }

            ClassLoaderCfg clCfg = new ClassLoaderCfg();
            clCfg.setIsSeparateClassLoaderNeeded(false);
            clCfg.setUserDefinedGroup("");
            clCfg.setFolderBasedGroup(false);

            PluginInterface pluginInterface =
                new Plugin(id, pluginType, mainClassTypesAnnotation.value(), category, description, tooltip,
                    iconFilename, false, nativePlugin, classMap, jarFiles, pluginFolder, version, enterprise, clCfg);
            registry.registerPlugin(pluginInterface);

            return pluginInterface;
        } catch (Throwable e) {
            throw new KettlePluginException(
                BaseMessages.getString(PKG, "BasePluginType.RuntimeError.UnableToReadPluginXML.PLUGIN0001"), e);
        }
    }

    protected String getTagOrAttribute(Node pluginNode, String tag) {
        String string = XMLHandler.getTagValue(pluginNode, tag);
        if (string == null) {
            string = XMLHandler.getTagAttribute(pluginNode, tag);
        }
        return string;
    }

    protected Map<String, String> readPluginLocale(Node pluginNode, String localizedTag, String translationTag) {
        Map<String, String> map = new Hashtable<String, String>();

        Node locTipsNode = XMLHandler.getSubNode(pluginNode, localizedTag);
        int nrLocTips = XMLHandler.countNodes(locTipsNode, translationTag);
        for (int j = 0; j < nrLocTips; j++) {
            Node locTipNode = XMLHandler.getSubNodeByNr(locTipsNode, translationTag, j);
            if (locTipNode != null) {
                String locale = XMLHandler.getTagAttribute(locTipNode, "locale");
                String locTip = XMLHandler.getNodeValue(locTipNode);

                if (!Utils.isEmpty(locale) && !Utils.isEmpty(locTip)) {
                    map.put(locale.toLowerCase(), locTip);
                }
            }
        }

        return map;
    }

    protected abstract String extractID(Annotation annotation);

    protected abstract String extractName(Annotation annotation);

    protected abstract String extractDesc(Annotation annotation);

    protected abstract String extractCategory(Annotation annotation);

    protected abstract String extractImageFile(Annotation annotation);

    @Deprecated
    protected abstract boolean extractSeparateClassLoader(Annotation annotation);

    protected abstract String extractI18nPackageName(Annotation annotation);

    protected abstract String extractSuggestion(java.lang.annotation.Annotation annotation);

    protected abstract String extractCasesUrl(java.lang.annotation.Annotation annotation);

    protected abstract String extractForumUrl(java.lang.annotation.Annotation annotation);

    @Deprecated
    protected String extractClassLoaderGroup(Annotation annotation) {
        return null;
    }

    protected abstract String extractVersion(Annotation annotation);

    protected abstract boolean extractEnterprise(Annotation annotation);

    protected abstract ClassLoaderCfg extractClassLoaderAnno(Annotation annotation);

    /**
     * When set to true the FluginFolder objects created by this type will be
     * instructed to search for additional plugins in the lib directory of plugin
     * folders.
     *
     * @param transverseLibDirs
     */
    protected void setTransverseLibDirs(boolean transverseLibDirs) {
        this.searchLibDir = transverseLibDirs;
    }

    protected synchronized void registerPluginJars() throws KettlePluginException {}

    /**
     * Handle an annotated plugin
     *
     * @param clazz            The class to use
     * @param annotation       The annotation to get information from
     * @param libraries        The libraries to add
     * @param nativePluginType Is this a native plugin?
     * @param pluginFolder     The plugin folder to use
     * @throws KettlePluginException
     */
    @Override
    public void handlePluginAnnotation(Class<?> clazz, Annotation annotation, List<String> libraries,
        boolean nativePluginType, URL pluginFolder) throws KettlePluginException {

        LdrPluginEntity pa = extractAnnotationInfo(clazz, annotation);
        String id = pa.getPluginId();
        Assert.notBlank(id, "No ID specified for plugin with class: {}.", clazz.getName());

        // Only one ID for now
        String name = pa.getName();
        String description = pa.getProfile();
        String category = pa.getCategory();
        String imageFile = pa.getImage();
        boolean separateClassLoader = pa.getClSeparateClassLoaderNeeded();
        String version = pa.getVersion();
        boolean enterprise = pa.getEnterprise();

        Map<Class<?>, String> classMap = new HashMap<Class<?>, String>();
        PluginMainClassType mainType = getClass().getAnnotation(PluginMainClassType.class);
        classMap.put(mainType.value(), clazz.getName());
        addExtraClasses(classMap, clazz, annotation);

        /*
         * PluginExtraClassTypes extraTypes =
         * this.getClass().getAnnotation(PluginExtraClassTypes.class); if(extraTypes !=
         * null){ for(int i=0; i< extraTypes.classTypes().length; i++){ Class<?>
         * extraClass = extraTypes.classTypes()[i]; // The extra class name is stored in
         * an annotation. // The name of the annotation is known //
         * ((RepositoryPlugin)annotation).dialogClass() String extraClassName =
         * extraTypes.classTypes()[i].getName();
         *
         * classMap.put(extraClass, extraClassName); } }
         */

        // 插件分类入库
        try {
            LdrClassificationEntity ldrClassificationEntity = classificationService.addDefault(this.getClass());
            LdrCategoryEntity ldrCategoryEntity = categoryService.addLocalCategory(ldrClassificationEntity, category);
            String pluginFolderName = FilenameUtils.getBaseName(pluginFolder.getPath());
            pluginService.addLocalPlugin(id, name, imageFile, description, pluginFolderName, ldrCategoryEntity);
        } catch (LaniusEngineDbException e) {
            throw new KettlePluginException(e);
        }

        ClassLoaderCfg clCfg = new ClassLoaderCfg();
        clCfg.setIsSeparateClassLoaderNeeded(pa.getClSeparateClassLoaderNeeded());
        clCfg.setUserDefinedGroup(pa.getClUserDefinedGroup());
        clCfg.setFolderBasedGroup(pa.getClFolderGroup());

        PluginInterface plugin =
            new Plugin(id, this.getClass(), mainType.value(), category, name, description, imageFile,
                separateClassLoader, nativePluginType, classMap, libraries, pluginFolder, version, enterprise, clCfg);
        ParentFirst parentFirstAnnotation = clazz.getAnnotation(ParentFirst.class);
        if (parentFirstAnnotation != null) {
            registry.addParentClassLoaderPatterns(plugin, parentFirstAnnotation.patterns());
        }
        registry.registerPlugin(plugin);

        if (libraries != null && libraries.size() > 0) {
            LogChannel.GENERAL.logDetailed(
                "Plugin with id [" + id + "] has " + libraries.size() + " libaries in its private class path");
        }
    }

    /**
     * Handle an annotated plugin
     *
     * @param clazz            The class to use
     * @param annotation       The annotation to get information from
     * @throws KettlePluginException
     */
    public LdrPluginEntity extractAnnotationInfo(Class<?> clazz, Annotation annotation) throws KettlePluginException {
        String altPackageName = clazz.getPackage().getName();
        String i18nPackageName = extractI18nPackageName(annotation);

        LdrPluginEntity pe = new LdrPluginEntity();
        pe.setPluginId(extractID(annotation));
        pe.setI18nPackageName(i18nPackageName);
        pe.setName(getTranslation(extractName(annotation), i18nPackageName, altPackageName, clazz));
        pe.setProfile(getTranslation(extractDesc(annotation), i18nPackageName, altPackageName, clazz));
        pe.setCategory(getTranslation(extractCategory(annotation), i18nPackageName, altPackageName, clazz));
        pe.setImage(extractImageFile(annotation));
        pe.setVersion(extractVersion(annotation).equals("none") ? Const.DEFAULT_VERSION : extractVersion(annotation));
        pe.setPluginTypeName(this.getClass().getName());
        pe.setEnterprise(extractEnterprise(annotation));

        pe.setMainClassName(clazz.getName());
        pe.setClSeparateClassLoaderNeeded(extractSeparateClassLoader(annotation));
        pe.setClUserDefinedGroup(extractClassLoaderGroup(annotation));

        ClassLoaderCfg cfg = extractClassLoaderAnno(annotation);
        if (cfg.getIsSeparateClassLoaderNeeded()) {
            pe.setClSeparateClassLoaderNeeded(cfg.getIsSeparateClassLoaderNeeded());
        }
        if (cfg.getFolderBasedGroup()) {
            pe.setClFolderGroup(cfg.getFolderBasedGroup());
        }
        if (StrUtil.isNotBlank(cfg.getUserDefinedGroup())) {
            pe.setClUserDefinedGroup(cfg.getUserDefinedGroup());
        }

        PluginMainClassType mainType = getClass().getAnnotation(PluginMainClassType.class);
        pe.setMainClassTypeName(mainType.value().getName());

        PluginAnnotationType annoType = getClass().getAnnotation(PluginAnnotationType.class);
        pe.setPluginAnnotationTypeName(annoType.value().getName());

        return pe;
    }

    public void registryPlugin(LdrPluginEntity ldrPluginEntity) throws KettlePluginException, MalformedURLException {
        Plugin plugin = Plugin.from(ldrPluginEntity);
        ParentFirst parentFirstAnnotation = ldrPluginEntity.getMainClass().getAnnotation(ParentFirst.class);
        if (parentFirstAnnotation != null) {
            registry.addParentClassLoaderPatterns(plugin, parentFirstAnnotation.patterns());
        }
        registry.registerPlugin(plugin);

        if (plugin.getLibraries() != null && plugin.getLibraries().size() > 0) {
            LogChannel.GENERAL.logDetailed("Plugin with id [" + id + "] has " + plugin.getLibraries().size()
                + " libaries in its private class path");
        }
    }

    @Override
    public void addExtraClasses(Map<Class<?>, String> classMap, Class<?> clazz, Annotation annotation) {}
}
